NoSQL 数据建模技术

最近实验室的科研项目用到了MongoDB数据库,上手十分简单,同学只是给我讲了十几分钟,我就能磕磕碰碰的使用了。当然只是建表,存,取等简单的功能。不过他的简单易用确实让我非常喜欢。作为NoSQL家族的代表作,我还是希望能更深入的学习一下。那么,就从NoSQL的数据建模技术谈起吧。

首先要介绍一个名词:索引

索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。 例如,employee 表的姓氏 (lname)列的值进行排序的结构。如果想按特定职员的姓来查找他或她,则与在表中搜索所有的行相比,索引有助于更快地获取信息。

例如这样一个查询:

select * from table1 whereid=10000

如果没有索引,必须遍历整个表,直到ID等于10000的这一行被找到为止;有了索引之后(必须是在ID这一列上建立的索引),即可在索引中查找。由于索引是经过某种算法优化过的,因而查找次数要少的多的多。可见,索引是用来定位的。

最普通的情况,是为出现在where子句的字段建一个索引。为方便讲述,先建立一个如下的表。 CREATE TABLE mytable( category_id int not null, user_id int not null, adddate int not null );

如果在查询时常用类似以下的语句:

SELECT * FROM mytable WHERE category_id=1;

最直接的应对之道,是为category_id建立一个简单的索引:

CREATE INDEX mytable_categoryid ON mytable (category_id);

OK.如果有不止一个选择条件呢?例如:

SELECT * FROM mytable WHERE category_id=1 AND user_id=2;

第一反应可能是,再给user_id建立一个索引。不好,这不是一个最佳的方法。可以建立多重的索引。

CREATE INDEX mytable_categoryid_userid ON mytable(category_id,user_id);

注意到在命名时的习惯了吗?使用表名---字段1名---字段2名的方式。重要的是数据库会真正用到这些索引。 要注意的是,建立太多的索引将会影响更新和插入的速度,因为它需要同样更新每个索引文件。对于一个经常需要更新和插入的表格,就没有必要为一个很少使用的where字句单独建立索引了,对于比较小的表,排序的开销不会很大,也没有必要建立另外的索引。

NoSQL概念

NoSQL有时也称作Not Only SQL的缩写,NoSQL 是对不同于传统的关联式数据库的数据库管理系统的统称。它打破了长久以来关系型数据库与ACID理论

ACID,是指在数据库管理系统(DBMS)中事务所具有的四个特性: 原子性 Atomicity : 例如转账操作,要么完成,要么不发生。 一致性 Consistency :同一数据的不同副本必须一致。 独立性 Isolation : 持久性 Durability :即使发生故障,数据需要保持原貌。

大一统的局面。两者存在许多显著的不同点,其中最重要的是NoSQL不使用SQL作为查询语言。其数据存储可以不需要固定的表格模式,也经常会避免使用SQL的JOIN操作,一般有水平可扩展性的特征.

水平扩展性(horizontal scalability)指能够连接多个软硬件的特性,这样可以将多个服务器从逻辑上看成一个实体。

我的感觉是,关系型数据库想把一致性,完整性,索引,CRUD都干好,NoSQL只干某一种或几种事,但是牺牲了很多别的东西。

NoSQL 数据存储不需要固定的表结构,通常也不存在连接操作。在大数据存取上具备关系型数据库无法比拟的性能优势。该术语在 2009 年初得到了广泛认同。

NoSQL与关系型数据库的区别

关系型数据库中的表都是存储一些格式化的数据结构,每个元组字段的组成都一样,即使不是每个元组都需要所有的字段,但数据库会为每个元组分配所有的字段,这样的结构可以便于表与表之间进行连接等操作,但从另一个角度来说它也是关系型数据库性能瓶颈的一个因素。而非关系型数据库以键值对存储,它的结构不固定,每一个元组可以有不一样的字段,每个元组可以根据需要增加一些自己的键值对,这样就不会局限于固定的结构,可以减少一些时间和空间的开销。

NoSQL的优势

  • 易扩展 NoSQL数据库种类繁多,但是一个共同的特点都是去掉关系数据库的关系型特性。数据之间无关系,这样就非常容易扩展。也无形之间,在架构的层面上带来了可扩展的能力。

  • 灵活的数据模型 NoSQL无需事先为要存储的数据建立字段,随时可以存储自定义的数据格式。而在关系数据库里,增删字段是一件非常麻烦的事情。如果是非常大数据量的表,增加字段简直就是一个噩梦。这点在大数据量的web2.0时代尤其明显。

NoSQL家族

要开始讨论数据建模技术,我们不得不或多或少地先系统地看一下NoSQL数据模型的成长的趋势,以此我们可以了解一些他们内在的联系。下图是NoSQL家族的进化图,我们可以看到这样的进化:Key-Value时代,BigTable时代,Document时代,全文搜索时代,和Graph数据库时代:

首先,我们需要注意的是SQL和关系型数据模型已存在了很长的时间,这种面向用户的自然性意味着:

  • 最终用户一般更感兴趣于数据的聚合显示,而不是分离的数据,这主要通过SQL来完成。

  • 我们无法通过人手工控制数据的并发性,完整性,一致性,或是数据类型校验这些东西的。这就是为什么SQL需要在事务,二维表结构(schema)和外表联合上做很多事。

另一方面,SQL可以让软件应用程序在很多情况下不需要关心数据库的数据聚合,和数据完整性和有效性进行控制。而如果我们去除了数据一致性,完整性这些东西,会对性能和分布存储有着重的帮助。正因为如此,我们才有数据模型的进化:

Key-Value

键值对存储是非常简单而强大的。下面的很多技术基本上都是基于这个技术开始发展的。但是,Key-Value有一个非常致命的问题,那就是如果我们需要查找一段范围内的key。(学过hash-table数据结构的人都应该知道,hash-table是非序列容器,其并不像数组,链接,队列这些有序容器,我们可以控制数据存储的顺序)。于是,有序键值 (Ordered Key-Value) 数据模型被设计出来解决这一限制,来从根本上提高数据集的问题。

Ordered Key-Value

有序键值模型也非常强大,但是,其也没有对Value提供某种数据模型。通常来说,Value的模型可以由应用负责解析和存取。这种很不方便,于是出现了BigTable类型的数据库.

BigTable

类型的数据库,这个数据模型其实就是map里有map,map里再套map,一层一层套下去,也就是层层嵌套的key-value(value里又是一个key-value),这种数据库的Value主要通过“列族”(column families),列,和时间戳来控制版本。(关于时间戳来对数据的版本控制主要是解决数据存储并发问题,也就是所谓的乐观锁)

Document databases

文档数据库改进了 BigTable模型,并提供了两个有意义的改善。第一个是允许Value中有主观的模式(scheme),而不是map套map。第二个是索引。 Full Text Search Engines 全文搜索引擎可以被看作是文档数据库的一个变种,他们可以提供灵活的可变的数据模式(scheme)以及自动索引。

Graph data models

图式数据库可以被认为是这个进化过程中从 Ordered Key-Value 数据库发展过来的一个分支。图式数据库允许构建议图结构的数据模型。它和文档数据库有关系的原因是,它的很多实现允许value可以是一个map或是一个document。

NoSQL的核心思想

NoSQL 数据模型设计一般从业务应用的具体数据查询入手,而不是数据间的关系:

关系型的数据模型基本上是分析数据间的结构和关系。其设计理念是: ”What answers do I have?”

NoSQL 数据模型基本上是从应用对数据的存取方式入手,如:我需要支持某种数据查询。其设计理念是 ”What questions do I have?”

NoSQL数据模型设计比关系型数据库需要对数据结构和算法的更深的了解。在这篇文章中我会和大家说那些尽人皆知的数据结构,这些数据结构并不只是被NoSQL使用,但是对于NoSQL的数据模型却非常有帮助。

关系型数据库对于处理层级数据和图式数据非常的不方便。NoSQL用来解决图式数据明显是一个非常好的解决方案,几乎所有的NoSQL数据库可以很强地解决此类问题。

下面是NoSQL的家族产品:

  • Key-Value 存储: Oracle Coherence, Redis, Kyoto Cabinet
  • 类BigTable存储: Apache HBase, Apache Cassandra
  • 文档数据库: MongoDB, CouchDB
  • 全文索引: Apache Lucene, Apache Solr
  • 图数据库: neo4j, FlockDB

NoSQL数据模型的基本原则

反规格化 Denormalization

反规格化是NoSQL最重要的设计原则之一。它可以被认为是把相同的数据拷贝到不同的文档或是表中,这样就可以简化和优化查询,或是正好适合用户的某中特别的数据模型。这篇文章中其他绝大多数原则都或多或少地导向了这一技术。

总体来说,反规格化需要权衡下面这些东西:

  • 查询数据量/查询IO VS总数据量 使用反规格化,一方面可以把一条查询语句所需要的所有数据组合起来放到一个地方存储。这意味着,其它不同查询所需要的相同的数据,需要放在不同的地方。因此,这产生了很多冗余的数据,从而导致了数据量的增大。

  • 处理复杂度 VS 总数据量 在符合范式的数据模式上进行表连接的查询,很显然会增加了查询处理的复杂度,尤其对于分布式系统来说更是。反规格化的数据模型允许我们以方便查询的方式来存构造数据结构以简化查询复杂度。

聚合 Aggregates

聚合是NoSQL另一个重要的设计原则。

所有类型的NoSQL数据库都会提供灵活的Schema(数据结构,对数据格式的限制):

  • Key-Value 和 Graph Databases基本上来说不会限制Value的形式,Value可以是任意格式。这样一来,这使得我们可以任意组合一个业务实体的keys。比如,我们有一个用户帐号的业务实体,其可以被如下这些key组合起来:
UserID_name, UserID_email, UserID_messages 

等等。如果一个用户没有email或message,那么相应也不会有这样的记录,它的值会留空。

  • BigTable 模型通过列集合来支持灵活的Schema,我们称之为列族(column family)。BigTable还可以在同一记录上出现不同的版本(通过时间戳)。

  • Document databases 文档数据库是一种层级式的“去Schema”的存储。

灵活的Schema允许你可以用一种嵌套式的内部数据方式来存储一组有关联的业务实体。这样可以为我们带来两个好处:

  • 最小化“一对多”关系——可以通过嵌套式的方式来存储实体,这样可以少一些表联结。

  • 可以让内部技术上的数据存储更接近于业务实体,特别是那种混合式的业务实体。可能存于一个文档集或是一张表中。

我们用下图的例子来解释这两种好处。 图中描给了电子商务中的商品模型。

首先,所有的商品Product都会有一个ID,Price 和 Description。

然后,我们可以知道不同的类型的商品会有不同的属性。比如,作者是书的属性,长度是牛仔裤的属性。其些属性可能是“一对多”或是“多对多”的关系,如:唱片中的曲目。

接下来,我们知道,某些业务实体不可能使用固定的类型。如:牛仔裤的属性并不是所有的牌子都有的,而且,有些名牌还会搞非常特别的属性。

对于关系型数据库来说,要设计这样的数据模型并不简单,如果我们想为所有的商品维护一个数据库,那必须定义完整的属性列表,不管该商品是否具有这个属性。就算能做到,也绝对离优雅很远很远。而我们NoSQL中灵活的Schema允许你使用Aggregate(product)建出所有不同种类的商品和他们的不同的属性。 但是我们可以看到在数据更新上,非规格化的数据存储在性能和一致性上会有很大的影响,这就是我们需要重点注意和不得不牺牲的地方。

应用层联结 Application Side Joins

表联结基本上不被NoSQL支持。正如我们前面所说的,NoSQL是“面向问题”而不是“面向答案”的,不支持表联结就是“面向问题”的后果。表的联结是在设计时被构造出来的,而不是在执行时建造出来的。所以,表联结在运行时是有很大开销的(搞过SQL表联结的都知道笛卡尔积是什么东西),但是在使用了Denormalization 和 Aggregates技术后,我们基本不用进行表联结,如:你们使用嵌套式的数据实体。当然,如果你需要联结数据,你需要在应用层完成这个事。下面是几个主要的Use Case:

  • 多对多的数据实体关系——经常需要被连接。

  • 聚合 Aggregates 并不适用于数据字段经常被改变的情况。对此,我们需要把那些经常被改变的字段分到另外的表中,而在查询时我们需要联结数据。例如,我们有个Message系统可以有一个User实体,其包括了一个内嵌的Message实体。但是,如果用户不断在附加 message,那么,最好把message拆分到另一个独立的实体,但在查询时联结这User和Message这两个实体。如下图:

通用建模技术 General Modeling Techniques

接下来,我们将讨论NoSQL中各种不同的通用的数据建模技术。

原子聚合 Atomic Aggregates

很多NoSQL的数据库(并不是所有)在事务处理(数据库应用中完成单一逻辑功能的操作集合)上都是短板。通常来说只能使用聚合Aggregates技术来保证一些ACID原则。

这就是为什么我们的关系型数据库需要有强大的事务处理机制——因为关系型数据库的数据是被规格化存放在了不同的地方。所以,Aggregates聚合允许我们把一个业务实体存成一个文档、存成一行,存成一个key-value,这样就可以原子式的更新了:

索引表 Index Table

Index Table 索引表是一个非常直白的技术,其可以你在不支持索引的数据库中得到索引的好处。BigTable是这类最重要的数据库。这需要我们维护一个有相应存取模式的特别表。例如,我们有一个主表存着用户帐号,其可以被UserID存取。某查询需要查出某个城市里所有的用户,于是我们可以加入一张表,这张表用城市做主键,所有和这个城市相关的UserID是其Value,如下所示: 可见,城市索引表的需要和对主表用户表保持一致性,因此,主表的每一个更新可能需要对索引表进行更新,不然就是一个批处理更新。无论哪个方式,这都会损伤一些性能,因为需要保持一致性。

Index Table 索引表可以被认为是关系型数据库中的视图的等价物。

键组合索引 Composite Key Index

Composite key 键组合是一个很常用的技术,对此,当我们的数据库支持键排序时能得到极大的好处。Composite key组合键的拼接成为第二排序字段可以让你构建出一种多维索引。例如,我们需要存取用户统计。如果我们需要根据不同的地区来统计用户的分布情况,我们可以把Key设计成这样的格式 (State:City:UserID),这样一来,就使得我们可以通过State到City来按组遍历用户,特别是我们的NoSQL数据库支持在key上按区查询(如:BigTable类的系统):

SELECT Values WHERE state="CA:*"
SELECT Values WHERE city="CA:San Francisco:*"

键组合聚合 Aggregation with Composite Keys

Composite keys 键组合技术并不仅仅可以用来做索引,同样可以用来区分不用的类型的数据以支持数据分组。考虑一个例子,我们有一个海量的日志数组,这个日志记录了互联网上的用户的访问来源。我们需要计算从某一网站过来的独立访客的数量,在关系型数据库中,我们可能需要下面这样的SQL查询语句:

SELECT count(distinct(user_id)) FROM clicks GROUP BY site

我们可以在NoSQL中建立如下的数据模型: 这样,我们就可以把数据按UserID来排序,我们就可以很容易把同一个用户的数据(一个用户并不会产生太多的event)进行处理,去掉那些重复的站点(使用hash table或是别的什么)。另一个可选的技术是,我们可以对每一个用户建立一个数据实体,然后把其站点来源追加到这个数据实体中,当然,这样一来,数据的更新在性能相比之下会有一定损失。

层级式模型 Hierarchy Modeling Techniques

树形聚合Tree Aggregation

树形或是任意的图(需反规格化)可以被直接打成一条记录或文档存放。

  • 当树形结构被一次性取出时这会非常有效率(如:我们需要展示一个blog的树形评论)
  • 搜索和任何存取这个实体都会存在问题。
  • 对于大多数NoSQL的实现来说,更新数据都是很不经济的(相比起独立结点来说)

Materialized Paths

Materialized Paths 可以帮助避免递归遍历(如:树形结构)。这个技术也可以被认为是反规格化的一种变种。其想法是为每个结点加上父结点或子结点的标识属性,这样就可以不需要遍历就知道所有的后裔结点和祖先结点了: 这个技术对于全文搜索引擎来说非常有帮助,因为其可以允许把一个层级结构转成一个文档。上面的示图中我们可以看到所有的商品或Men’s Shoes下的子分类可以被一条很短的查询语句处理——只需要给定个分类名。

Materialized Paths 可以存储一个ID的集合,或是一堆ID拼出的字符串。后者允许你通过一个正则表达式来搜索一个特定的分支路径。下图展示了这个技术(分支的路径包括了结点本身):

嵌套集 Nested Sets

Nested sets 嵌套集是树形结构的标准技术。它被广泛地用在了关系性数据库中,它完全地适用于 Key-Value 键值对数据库 和 Document Databases 文档数据库。这个技术的想法是把叶子结点存储成一个数组,并通过使用索引的开始和结束来映射每一个非叶子结点到一个叶子结点集,就如下图所示一样: 这样的数据结构对于immutable data不变的数据 有非常不错的效率,因为其点内存空间小,并且可以很快地找出所有的叶子结点而不需要树的遍历。尽管如此,在插入和更新上需要很高的性能成本,因为新的叶子结点需要大规模地更新索引。

嵌套文档扁平化:有限的字段名 Nested Documents Flattening: Numbered Field Names

搜索引擎基本上来说和扁平文档一同工作,如:每一个文档是一个扁平的字段和值的列表。这种数据模型的用来把业务实体映射到一个文本文档上,如果你的业务实体有很复杂的内部结构,这可能会变得很有挑战。一个典型的挑战是把一个有层级的文档映映射出来。例如,文档中嵌套另一个文档。让我们看看下面的示例: 我把这个层级文档映射成一个文本文档,一种方法是创建 Skill 和 Level 字段。这个模型可以通过skill或是level来搜索一个人,而上图标注的那样的组合查询则会失败,因为分不清Excellent是否是Math还是Poetry上的.

一种解决方案是,其为每个字段都标上数字 Skill_i 和 Level_i,这样就可以分开搜索每一个对(下图中使用了OR来遍历查找所有可能的字段):

嵌套文档扁平化:邻近查询 Nested Documents Flattening: Proximity Queries

还有一种技术用来解决扁平层次文档。它用邻近的查询来限制可被查询的单词的范围。下图中,所有的技能和等级被放在一个字段中,叫 SkillAndLevel,查询中出现的 “Excellent” 和 “Poetry” 必需一个紧跟另一个:

Comments !